/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package com.google.code.peersim.pastry.controls;

import com.google.code.peersim.pastry.protocol.PastryJoinLsnrIfc;
import com.google.code.peersim.pastry.protocol.PastryNode;
import com.google.code.peersim.pastry.protocol.PastryProtocol;
import com.google.code.peersim.pastry.protocol.PastryProtocolState;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.util.HashSet;
import java.util.Set;
import peersim.config.Configuration;
import peersim.core.CommonState;
import peersim.core.Control;
import peersim.core.Network;
import peersim.util.FileNameGenerator;
import peersim.util.IncrementalStats;

/**
 * This {@link Control} class is in charge of building the Pastry-network by asking
 * nodes to progressively join the system. A node joins the network by sending a special
 * {@code join} message to its closest node (every node is assigned to its closest one
 * by the {@link TwoDimClosestNodeInitializer} control-class). Since the system is
 * intially empty, this control-class does what follows iterating over the {@link Network}:
 * <ol>
 * <li>peeks the closest neighbor of the first node stored in the {@link Network} singleton
 * and sets its state to {@link PastryProtocolState#JOINED}</li>
 * <li>tells the first node stored in {@link Network} to try and join the system</li>
 * <li>iterating over the network, asks each node whose {@link PastryProtocol}
 * instance is in state {@link PastryProtocolState#INITIALIZED} for its closest node and
 * asks the latter to join the system iff it is in state {@link PastryProtocolState#INITIALIZED},
 * after that asks the former to do the same</li>
 * </ol>
 *
 * @author frusso
 * @version 0.1
 * @since 0.1
 */
public class PastryOverlayBuilder implements Control, PastryJoinLsnrIfc {

  /**
   * Configuration parameter defining the file this component should log to.
   */
  public static final String LOG_FILE = "log";
  /**
   * Configuration parameter defining whether this component should log or not.
   */
  public static final String DO_LOG = "doLog";
  /**
   * Is this the first node being joined?
   */
  private static boolean isFirstNodeEver = true;

  private static Set<OverlayBuilderListenerIfc> overlayConstructionListeners = new HashSet<OverlayBuilderListenerIfc>();
  
  /**
   * States whether the network has been built or not.
   *
   * @return Whether the network has been built or not
   */
  private static boolean isBuilt() {
    int n = Network.size();
    for(int i=0; i<n; i++) {
      PastryNode node = (PastryNode) Network.get(i);
      if(node.isJoinable())
        return false;
    }
    return true;
  }

  public static void addOverlayBuilderListener(OverlayBuilderListenerIfc lsnr) {
    overlayConstructionListeners.add(lsnr);
  }

  public static boolean removeOverlayBuilderListener(OverlayBuilderListenerIfc lsnr) {
    return overlayConstructionListeners.remove(lsnr);
  }

  private PrintStream stream;
  private String fileName;
  private IncrementalStats stats;
  private boolean doLog;

  /**
   * Constructor.
   * 
   * @param prefix
   */
  public PastryOverlayBuilder(String prefix) throws FileNotFoundException {
    doLog = Configuration.getBoolean(prefix + "." + DO_LOG);
    if(doLog) {
      fileName = new FileNameGenerator(Configuration.getString(prefix + "."+LOG_FILE), ".log").nextCounterName();
      stream = new PrintStream(new FileOutputStream(fileName));
    }
    stats = new IncrementalStats();
  }

  /**
   * {@inheritDoc}
   *
   * Executes until the network has not been completely built.
   */
  @Override
  public boolean execute() {
    boolean stop = false;
    if(!isBuilt()) {
      joinFirstJoinableNode();
    } else {
      notifyOverlayConstructionListeners();
    }
    if(CommonState.getTime()==CommonState.getEndTime()-1) {
      printFinalStats();
    }
    return stop;
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void joined(JoinedInfo info) {
    if(doLog) {
      stream.println(info.getNodeId() +" "+ info.getHops());
    }
    stats.add(info.getHops());
    printStats();
  }

  /**
   * Forces the given node to {@link PastryProtocolState#JOINED} state iff it not
   * already in.
   * 
   * @param node The node whose state must be changed
   */
  private void forceJoinStateFor(PastryNode node) {
    if(!node.isJoined())
        node.setJoined();
  }

  /**
   * Peeks a {@link PastryNode} from the network and, if it is the very first peeked
   * node its state is forced to {@link PastryProtocolState#JOINED}, otherwise that node
   * is asked to start joining the system. If there are no more joinable nodes
   * the network is considered as built.
   */
  private void joinFirstJoinableNode() {
    PastryNode joinableNode = peekJoinableNode();
    if(joinableNode!=null) {
      if(isFirstNodeEver) {
        forceJoinStateFor(joinableNode);
        isFirstNodeEver = false;
      } else {
        startJoining(joinableNode);
      }
    }
  }

  private void notifyOverlayConstructionListeners() {
    for(OverlayBuilderListenerIfc lsnr : overlayConstructionListeners) {
      lsnr.overlayBuilt();
    }
  }

  /**
   * Selects a {@link PastryNode} from the network that is in state
   * {@link PastryProtocolState#INITIALIZED} and returns it.
   *
   * @return A joinable node
   */
  private PastryNode peekJoinableNode() {
    PastryNode joinable = null;
    int n = Network.size();
    for(int i=0; i<n; i++) {
      PastryNode node = (PastryNode) Network.get(i);
      if(node.isJoinable()) {
        joinable = node;
        break;
      }
    }
    return joinable;
  }

  private void printFinalStats() {
    System.err.println(this.getClass().getName()+":\nAvg-hops: "+stats.getAverage()+"\nMax-hops: "+stats.getMax()+"\nMin-hops: "+stats.getMin()+"\nStddev-hops: "+stats.getStD()+"\nVar-hops: "+stats.getVar()+"\n");
  }

  private void printStats() {
    if(doLog)
      stream.println("Avg-hops: "+stats.getAverage()+" Max-hops: "+stats.getMax()+" Min-hops: "+stats.getMin()+" Stddev-hops: "+stats.getStD()+" Var-hops: "+stats.getVar());
  }

  /**
   * Helper method for scheduling a join event for the given node.
   *
   * @param node The node that must be joined
   */
  private void startJoining(PastryNode node) {
    node.addJoinListener(this);
    node.getPastryProtocol().startJoining(node,0);
  }

  /**
   * @author frusso
   * @version 0.1
   * @since 0.1
   */
  public static interface OverlayBuilderListenerIfc {

    /**
     * Notifies the costruction of the overlay network has been accomplished.
     */
    public void overlayBuilt();
  }
}